﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.Shell.Implementation.DefaultCodeEditor
{

    using System;
    using System.Drawing;
    using System.Windows.Forms.Integration;
    using CodeBoxControl;
    using CodeBoxControl.Decorations;
    using ConnectBasic.Shell.Extensibility;

    /// <summary>
    /// Provides support for editing code documents.
    /// </summary>
    internal class CodeEditor : IEditor, IHighlightService
    {
        private ICodeDocument mDocument;

        // The code box
        private ElementHost mCodeBoxHost;
        private CodeBox mCodeBox;
        private ExplicitDecoration mHighlightDecorationBG;
        private ExplicitDecoration mHighlightDecorationFG;

        // The current code buffer
        private CodeEditorCodeSnapshot mCurrentSnapshot;

        #region Events

        private EventHandler<CodeEditorEventArgs> mBufferChanged;

        /// <summary>
        /// Raised when the code buffer changes.
        /// </summary>
        public event EventHandler<CodeEditorEventArgs> BufferChanged
        {
            add
            {
                if (value == null)
                {
                    return;
                }

                // Attach our event handle to the text box if appropriate
                if (mBufferChanged == null)
                {
                    mCodeBox.TextChanged += new System.Windows.Controls.TextChangedEventHandler(mCodeBox_TextChanged);
                }

                // Update the delegate
                mBufferChanged += value;

            }
            remove
            {
                // Update the delegate
                mBufferChanged -= value;

                // If there are no event handlers left, detach our event handler from
                // the text box
                if (mBufferChanged == null)
                {
                    mCodeBox.TextChanged -= new System.Windows.Controls.TextChangedEventHandler(mCodeBox_TextChanged);
                }
            }
        }

        #endregion

        #region .ctor

        /// <summary>
        /// Initializes a new instance of the <see cref="CodeEditor" />
        /// class.
        /// </summary>
        /// <param name="document">The document to edit.</param>
        public CodeEditor(ICodeDocument document)
        {
            if (document == null) throw new ArgumentNullException("document");
            mDocument = document;

            // Create the code box state 
            mCodeBox = new CodeBox();
            mCodeBox.AcceptsReturn = true;
            mCodeBox.AcceptsTab = true;
            mCodeBox.FontFamily = new System.Windows.Media.FontFamily("Consolas");
            mCodeBox.HorizontalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Visible;
            mCodeBox.VerticalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Visible;
            mCodeBox.BorderThickness = new System.Windows.Thickness(0);

            // Setup the element host and stop it sending certain properties
            // to the element
            mCodeBoxHost = new ElementHost();
            mCodeBoxHost.Child = mCodeBox;
            mCodeBoxHost.PropertyMap.Remove("BackColor");

            // The CodeBox has its background set to be transparent so 
            // we need to ensure that the background color of the host
            // is set to white.
            mCodeBoxHost.BackColor = SystemColors.Window;
        }

        #endregion

        #region IEditor Members

        public System.Windows.Forms.Control GetEditControl()
        {
            return mCodeBoxHost;
        }

        public string Title
        {
            get { return mDocument.Title; }
        }

        public string FileName
        {
            get { return mDocument.FileName; }
        }

        #endregion

        #region IServiceProvider Members

        public object GetService(Type serviceType)
        {
            if (serviceType == typeof(IHighlightService))
            {
                return this;
            }

            return null;
        }

        #endregion

        #region IHighlightService Members

        public int SetHighlight(string definitionName, int startLine, int startColumn, int endLine, int endColumn)
        {
            if (definitionName == "CurrentLine")
            {

                // Apply it
                SetCurrentLineHighlight(startLine, startColumn, endLine, endColumn);

                // Always refer to the current line highlight as id 0
                return 0;
            }

            // Not set
            return -1;
        }

        public void ClearHighlight(int id)
        {

            // Is it the current line highlight?
            if (id == 0)
            {

                // Remove the decorations for the current line highlight
                mCodeBox.Decorations.Remove(mHighlightDecorationBG);
                mHighlightDecorationBG = null;

                mCodeBox.Decorations.Remove(mHighlightDecorationFG);
                mHighlightDecorationFG = null;

                // Update the UI
                mCodeBox.InvalidateVisual();
            }
        }

        #endregion

        #region Event Handlers

        void mCodeBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {

            // If we've got any services running in the background, stop them now
            mCurrentSnapshot = null;

            // Raise the buffer changed event
            EventHandler<CodeEditorEventArgs> evt = mBufferChanged;

            if (evt != null)
            {

                // Create a new code snapshot
                mCurrentSnapshot = new CodeEditorCodeSnapshot(mCodeBox.Text);

                // Raise the event
                evt(this, new CodeEditorEventArgs(mCurrentSnapshot));

                // See what services we can get from it now
                Colorize(mCurrentSnapshot);
            }
        }

        #endregion

        #region Private Methods

        private void SetCurrentLineHighlight(int startLine, int startColumn, int endLine, int endColumn)
        {
            
            // Make sure we've got a decoration
            if (mHighlightDecorationBG == null)
            {
                mHighlightDecorationBG = new ExplicitDecoration()
                {
                    Brush = System.Windows.Media.Brushes.Yellow,
                    DecorationType = EDecorationType.Hilight
                };

                mCodeBox.Decorations.Add(mHighlightDecorationBG);

                mHighlightDecorationFG = new ExplicitDecoration()
                {
                    Brush = System.Windows.Media.Brushes.Black,
                    DecorationType = EDecorationType.TextColor
                };

                mCodeBox.Decorations.Add(mHighlightDecorationFG);
            }        

            // -1 Because source locations are 1 based
            startLine--;
            startColumn--;
            endLine--;
            endColumn--;

            int startIndex = mCodeBox.GetCharacterIndexFromLineIndex(startLine) + startColumn;
            int endIndex = mCodeBox.GetCharacterIndexFromLineIndex(endLine) + endColumn;
            int length = endIndex - startIndex;

            // Update the highlights
            mHighlightDecorationBG.Start = mHighlightDecorationFG.Start = startIndex;
            mHighlightDecorationBG.Length = mHighlightDecorationFG.Length = length;

            // Make sure the code box repaints itself
            mCodeBox.InvalidateVisual();
        }

        private void Colorize(CodeEditorCodeSnapshot snapshot)
        {
            // Get the categorizer service
            var categorizer = snapshot.GetService<ICategorizer>();

            if (categorizer == null)
            {
                return;
            }

            // TODO: Threading & decoration diffing

            // Clear existing decorations
            mCodeBox.Decorations.Clear();

            // Categorize only while it's appropriate to do so
            var spanEnum = categorizer.GetSpanEnumerator();

            while (mCurrentSnapshot == snapshot && spanEnum.MoveNext())
            {
                var decoration = MakeDecoration(spanEnum.Current);

                if (decoration != null)
                {
                    mCodeBox.Decorations.Add(decoration);
                }
            }
        }

        private ExplicitDecoration MakeDecoration(ISpan span)
        {

            // TODO: Settings
            switch (span.CategoryName)
            {
                case "StringLiteral":
                    return new ExplicitDecoration()
                    {
                        Brush = System.Windows.Media.Brushes.IndianRed,
                        Start = span.StartIndex,
                        Length = span.EndIndex - span.StartIndex
                    };

                case "LineComment":
                    return new ExplicitDecoration()
                    {
                        Brush = System.Windows.Media.Brushes.Green,
                        Start = span.StartIndex,
                        Length = span.EndIndex - span.StartIndex
                    };

                case "NumericLiteral":
                    return new ExplicitDecoration()
                    {
                        Brush = System.Windows.Media.Brushes.DarkGray,
                        Start = span.StartIndex,
                        Length = span.EndIndex - span.StartIndex
                    };

                case "Keyword":
                    return new ExplicitDecoration()
                    {
                        Brush = System.Windows.Media.Brushes.Blue,
                        Start = span.StartIndex,
                        Length = span.EndIndex - span.StartIndex
                    };

                default:
                    return null;
            }
        }

        #endregion

    }
}
